
from __future__ import with_statement

import os
import sys
import cgi
import logging
from inspect import getargspec
from itertools import chain
from contextlib import contextmanager

import pkg_resources
import sqlalchemy
import transaction as zope_transaction
from webob import Request, Response, MultiDict, NestedMultiDict
from paste.deploy import appconfig

from clutch.globals import C, G, ContextClass
from clutch.config import ConfigObj
from clutch.proxy import object_manager
from clutch import middleware as mw
from clutch import exceptions as exc
# from clutch import database as db
from decorators import Anno

log = logging.getLogger(__name__)

def make_app_test(global_config, **local_config):
    '''This sets up some reasonable middleware for testing'''
    base_app = app = make_app_no_middleware(global_config, **local_config)
    config = app.config
    app = mw.make_session_middleware(app, global_config, **config.session)
    app = mw.make_cache_middleware(app, global_config, **config.cache)
    app = mw.make_transaction_middleware(app, global_config,
                                         **config.transaction)
    app = mw.make_http_error_middleware(app, global_config)
    app = mw.make_dbsession_middleware(app, global_config, **config.dbsession)
    return app

def make_app_main(global_config, **local_config):
    '''This assumes that middleware.ini is responsible for setting
    up the middleware'''
    base_app = app = make_app_no_middleware(global_config, **local_config)
    config = app.config
    if global_config.get('mode') == 'development':
        app = mw.development_cascade(app, base_app, config.clutch.view)
    return app

def make_app_no_middleware(global_config, **local_config):
    # Setup config
    config = ConfigObj()
    config._configure(**global_config)
    config._configure(**local_config)
    # Load entry points
    app_config = load_app_config(config.clutch.app_config)
    return WSGIApplication(config, app_config)

def load_app_config(requirement):
    if '#' in requirement:
        egg, name = requirement.split('#')
    else:
        egg, name = requirement, 'main'
    if egg:
        emap = pkg_resources.get_entry_map(egg, 'clutch.application')
        ep = emap[name]
    else:
        for ep in pkg_resources.iter_entry_points(
            'clutch.application', name):
            break
    app_config_class = ep.load()
    app_config = app_config_class(ep.module_name)
    return app_config

class WSGIApplication(object):

    def __init__(self, config, app_config, in_wsgi_server=True):
        self.config = config
        self.app_config = app_config
        self.in_wsgi_server = in_wsgi_server
        self.G = ContextClass()
        self.G.application = self
        self.G.config = config
        with self.context(in_init=True):
            if app_config.model:
                log.info('Load model: %s', app_config.package_name)
                self.model = app_config.model.load()
                log.info('Freeze model: %s', app_config.package_name)
                self.model.environ.freeze()
                log.info('Playback model: %s', app_config.package_name)
                self.model.environ.playback()
            else:
                self.model = None
            self.root = app_config.root()
            self.commands = app_config.commands.load()
        self.api = {}
        for name, app in config.clutch.api.items():
            if isinstance(app, basestring):
                if app.startswith('config:'):
                    cfg = appconfig(app, 'main', relative_to=config.here)
                else:
                    cfg = appconfig('config:' + config.__file__, app)
                self.api[name] = make_app_no_middleware(
                    cfg.global_conf,
                    **cfg.local_conf)
            else:
                self.api[name] = make_app_no_middleware(
                    config.global_conf,
                    **app)

    @classmethod
    def create(klass, config_file, app_name, in_wsgi_server=True):
        config_file = os.path.abspath(config_file)
        cfg = appconfig('config:' + config_file, app_name)
        config = ConfigObj()
        config._configure(**cfg)
        # Load entry points
        app_config = load_app_config(config.clutch.app_config)
        return klass(config, app_config, in_wsgi_server=in_wsgi_server)

    def __call__(self, environ, start_response):
        with self.context(environ=environ):
            try:
                C.request = req = Request(environ)
                C.response = res = Response(headerlist=[])
                C.template = None
                C.controller = None
                C.session = environ.get('beaker.session')

                # Parse the path
                if req.path_info == '':
                    err = exc.HTTPMovedPermanently(location=req.path + '/')
                    return err(environ, start_response)
                assert req.path_info.startswith('/')
                remainder = req.path_info[1:].split('/')
                root = self.root
                cstack = [  ]
                while True:
                    # Find the controller
                    try:
                        try:
                            controller, remainder = self._find_controller(
                                root, remainder, cstack)
                        except:
                            controller = self._error_handler(cstack)
                        C.controller = controller
                        while True:
                            # Dispatch to the controller
                            try:
                                res = self._dispatch_request(
                                    req, res, controller, remainder)
                                return res(environ, start_response)
                            except exc.ContinueTraversal, ct:
                                raise
                            except:
                                controller = self._error_handler(cstack)
                    except exc.ContinueTraversal, ec:
                        root, remainder = ct.args
            finally:
                # self.model.session.remove()
                pass

    def call_api(self, path_info, *args, **kwargs):
        with self.context():
            if C.in_transaction:
                try:
                    self.model.session.begin()
                except sqlalchemy.exc.InvalidRequestError:
                    pass
            else:
                import pdb; pdb.set_trace()
            remainder = path_info[1:].split('/')
            root = self.root
            cstack = [  ]
            while True:
                # Find the controller
                try:
                    try:
                        controller, remainder = self._find_controller(
                            root, remainder, cstack, api=True)
                    except:
                        controller = self._error_handler(cstack)
                    C.controller = controller
                    while True:
                        # Dispatch to the controller
                        try:
                            res = self._dispatch_call(
                                controller,
                                *(list(remainder)+list(args)),
                                **kwargs)
                            return res
                        except exc.ContinueTraversal, ct:
                            raise
                        except:
                            controller = self._error_handler(cstack)
                except exc.ContinueTraversal, ec:
                    root, remainder = ct.args
            
    def _error_handler(self, cstack):
        exc_info = sys.exc_info()
        C.exc_info = exc_info
        try:
            top, remainder = cstack.pop()
            return top._error_handler(exc_info)
        except IndexError:
            raise exc_info[0], exc_info[1], exc_info[2]

    def _dispatch_call(self, controller, *args, **kwargs):
        return controller(*args, **kwargs)

    def _dispatch_request(self, req, res, controller, remainder):
        anno = Anno.get(controller)
        if anno.needs_txn:
            txn = zope_transaction.get()
            try:
                C.in_transaction = True
                if self.model:
                    self.model.session.begin()
            except sqlalchemy.exc.InvalidRequestError:
                pass
        anno.check_authorization()
        params = req.params
        if (req.method in ('PUT', 'DELETE')
            and req.content_type in ('application/x-www-form-urlencoded',
                                     'multipart/form-data')):
            fs = cgi.FieldStorage(environ=req.environ, fp=req.body_file)
            md = MultiDict.from_fieldstorage(fs)
            params = NestedMultiDict(req.GET, md)
        params = anno.decode_params(params)
        params = anno.validate_params(params)
        args, kwargs = self._coerce_args(
            controller, remainder, params)
        resp_obj = controller(*args, **kwargs)
        anno = Anno.get(C.controller)
        content_type, renderer, template = anno.get_renderer(req.accept)
        if C.template is not None:
            template = C.template
        app_iter = renderer(template, resp_obj)
        res.status = 200
        if 'content-type' not in res.headers:
            res.headerlist.append(('content-type', content_type))
        else:
            import pdb; pdb.set_trace()
        if res.headers['content-type'] is None:
            import pdb; pdb.set_trace()
        if anno.needs_txn:
            body = ''.join(app_iter)
            if isinstance(body, unicode):
                charset_matches = req.accept_charset.best_matches()
                if not charset_matches:
                    charset_matches = ['utf-8']
                res.charset = charset_matches[0]
                res.unicode_body = body
            else:
                res.body = body
            txn.commit()
        else:
            res.app_iter = app_iter
        if C.session:
            C.session.save()
        return res

    @contextmanager
    def context(self, environ=None, in_init=False):
        c_mgr = object_manager(C)
        c_mgr.push()
        C.G = self.G
        if not in_init and self.model:
            # Save the SA session in the environ to be removed later
            if hasattr(C, 'request'):
                environ = C.request.environ
            else:
                environ = environ
            if environ is not None:
                om = object_manager(self.model.session)
                environ['clutch.sa_sessions'].add(om.obj)
        try:
            yield self
        finally:
            c_mgr.pop()

    def _find_controller(self, root, path, controller_stack, api=False):
        cur = root
        while path:
            if not traversal_allowed(cur, api):
                if C.request.environ.get('REMOTE_USER') is None:
                    raise exc.HTTPUnauthorized()
                else:
                    raise exc.HTTPForbidden()
            if is_controller(cur, api): return cur, path
            controller_stack.append((cur, path)) # for handling errors
            next, rest = path[0], path[1:]
            if next == '':
                next = 'index'
            try:
                cur = getattr(cur, next)
                path = rest
            except AttributeError:
                raise exc.HTTPNotFound()
        if is_controller(cur, api): return cur, path
        if hasattr(cur, 'index'):
            if is_controller(cur.index, api):
                raise exc.HTTPMovedPermanently(add_slash=True)
        raise exc.HTTPNotFound()

    def _coerce_args(self, controller, path_remainder, params):
        try:
            args, varargs, varkw, defaults = getargspec(controller)
        except TypeError:
            args, varargs, varkw, defaults = getargspec(controller.__init__)
        positional = path_remainder
        for a in args[:len(positional)]:
            if a in params:
                del params[a]
        return positional, params

def traversal_allowed(obj, api):
    if api: return True
    requirements = getattr(obj, '_require', [])
    for r in requirements:
        if not r(): return False
    return True

def is_controller(obj, api):
    try:
        if not callable(obj): return False
        anno = Anno.get(obj)
        if anno.exposed: return True
        if api and anno.exposed_api: return True
        return False
    except AttributeError:
        return False
